/**
 * This file is part of the eConference project and it is distributed under the 
 * terms of the MIT Open Source license.
 * 
 * The MIT License
 * Copyright (c) 2005 Collaborative Development Group - Dipartimento di Informatica, 
 *                    University of Bari, http://cdg.di.uniba.it
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this 
 * software and associated documentation files (the "Software"), to deal in the Software 
 * without restriction, including without limitation the rights to use, copy, modify, 
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to the following 
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies 
 * or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 
 * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package it.uniba.di.cdg.xcore.multichat.jabber;

import it.uniba.di.cdg.jabber.IPacketExtension;
import it.uniba.di.cdg.jabber.InvitationEvent;
import it.uniba.di.cdg.jabber.JabberBackend;
import it.uniba.di.cdg.jabber.PacketExtensionAdapter;
import it.uniba.di.cdg.jabber.internal.XMPPUtils;
import it.uniba.di.cdg.smackproviders.PrivateMessageIQPacket;
import it.uniba.di.cdg.smackproviders.SmackCommons;
import it.uniba.di.cdg.smackproviders.TypingNotificationPacket;
import it.uniba.di.cdg.smackproviders.ViewReadOnlyPacket;
import it.uniba.di.cdg.xcore.aspects.ThreadSafetyAspect;
import it.uniba.di.cdg.xcore.multichat.events.IManagerEventListener;
import it.uniba.di.cdg.xcore.multichat.events.ViewReadOnlyEvent;
import it.uniba.di.cdg.xcore.multichat.model.ChatRoomModel;
import it.uniba.di.cdg.xcore.multichat.model.IChatRoomModel;
import it.uniba.di.cdg.xcore.multichat.model.IParticipant;
import it.uniba.di.cdg.xcore.multichat.model.Participant;
import it.uniba.di.cdg.xcore.multichat.model.IParticipant.Role;
import it.uniba.di.cdg.xcore.multichat.model.IParticipant.Status;
import it.uniba.di.cdg.xcore.multichat.service.IInvitationRejectedListener;
import it.uniba.di.cdg.xcore.multichat.service.IMessageReceivedListener;
import it.uniba.di.cdg.xcore.multichat.service.IMultiChatService;
import it.uniba.di.cdg.xcore.multichat.service.IRoomInfo;
import it.uniba.di.cdg.xcore.multichat.service.IUserStatusListener;
import it.uniba.di.cdg.xcore.multichat.service.MultiChatContext;
import it.uniba.di.cdg.xcore.multichat.service.MultiChatMessage;
import it.uniba.di.cdg.xcore.network.IBackend;
import it.uniba.di.cdg.xcore.network.events.ITypingEventListener;
import it.uniba.di.cdg.xcore.network.events.TypingEvent;
import it.uniba.di.cdg.xcore.network.messages.IMessage;
import it.uniba.di.cdg.xcore.network.model.tv.Entry;
import it.uniba.di.cdg.xcore.network.model.tv.ITalkModel;
import it.uniba.di.cdg.xcore.network.model.tv.TalkModel;
import it.uniba.di.cdg.xcore.network.services.INetworkServiceContext;
import it.uniba.di.cdg.xcore.network.services.NetworkServiceException;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.muc.DefaultParticipantStatusListener;
import org.jivesoftware.smackx.muc.DefaultUserStatusListener;
import org.jivesoftware.smackx.muc.DiscussionHistory;
import org.jivesoftware.smackx.muc.InvitationRejectionListener;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.muc.Occupant;
import org.jivesoftware.smackx.muc.ParticipantStatusListener;
import org.jivesoftware.smackx.muc.RoomInfo;
import org.jivesoftware.smackx.muc.SubjectUpdatedListener;
import org.jivesoftware.smackx.muc.UserStatusListener;

/**
 * Jabber/XMPP implementation of the multi-chat service. 
 * <p>
 * Note that we provide additional methods for realizing the IoC build-up of instances 
 * of this class (they are not part of {@see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService}
 * interface specification but only of this class: they are "plug-in-locally" designed. 
 */
public class JabberMultiChatService implements IMultiChatService {
    /**
     * Jabber/XMPP adapter implementation.
     */
    private static class RoomInfoAdapter implements IRoomInfo {
        /**
         * Wrapped smack room info.
         */
        private RoomInfo smackRoomInfo;

        public RoomInfoAdapter( RoomInfo smackRoomInfo ) {
            this.smackRoomInfo = smackRoomInfo;
        }

        /* (non-Javadoc)
         * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService.IRoomInfo#getDescription()
         */
        public String getDescription() {
            return smackRoomInfo.getDescription();
        }

        /* (non-Javadoc)
         * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService.IRoomInfo#getRoomId()
         */
        public String getRoomId() {
            return smackRoomInfo.getRoom();
        }

        /* (non-Javadoc)
         * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService.IRoomInfo#getSubject()
         */
        public String getSubject() {
            return smackRoomInfo.getSubject();
        }

        /* (non-Javadoc)
         * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService.IRoomInfo#isMembersOnly()
         */
        public boolean isMembersOnly() {
            return smackRoomInfo.isMembersOnly();
        }

        /* (non-Javadoc)
         * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService.IRoomInfo#isModerated()
         */
        public boolean isModerated() {
            return smackRoomInfo.isModerated();
        }
    }

    /**
     * Filters typing packets for multichats. We need this since that typing packet is implemented 
     * as a Message and we need a way to distinguish them from normal packets.
     */
    private static class MultiChatTypingFilter extends AndFilter {
        /**
         * Makes up a new filter for monitoring typing events in multichats. The parameter <code>from</code> 
         * is needed since the server will notify the same emitter of the notification packet :O and
         * we don't want this to happen.
         * 
         * @param acceptTo the user id to accept in incoming packet field <code>to</code>
         * @param ignoreFrom the user id to ignore in incoming packet field <code>from</code>
         */
        public MultiChatTypingFilter( final String acceptTo, final String ignoreFrom ) {
            super();
            addFilter( new PacketTypeFilter( Message.class ) );
            addFilter( TypingNotificationPacket.FILTER );
            addFilter( new PacketFilter() {
                public boolean accept( Packet packet ) {
                    return acceptTo.equals( packet.getTo() );
                }
            } );
            addFilter( new PacketFilter() {
                public boolean accept( Packet packet ) {
                    return !ignoreFrom.equals( packet.getFrom() );
                }
            } );
        }
    }

    /**
     * Listeners for notifying the service status.
     */
    private final List<IMultiChatServiceListener> multiChatListeners;

    /**
     * Listeners for invitation refusal.
     */
    private final List<IInvitationRejectedListener> invitationRejectedListeners;

    /**
     * Listeners for changes in the local user status.
     */
    private final List<IUserStatusListener> userStatusListeners;

    /**
     * Clients for getting back the messages ...
     */
    private final List<IMessageReceivedListener> messageReceivedListeners;

    /**
     * Typing event listeners for this chat.
     */
    private final List<ITypingEventListener> typingListeners;

    /**
     * Listener for manager events.
     */
    private final List<IManagerEventListener> managerEventListeners;

    /**
     * The chat room: clients may want to register as listeners to this.
     */
    private IChatRoomModel chatRoomModel;

    /**
     * The talk model tracks the message entries.
     */
    private ITalkModel talkModel;

    /**
     * Listen to SMACK invitation rejections (remote clients that rejects our invitations).
     */
    private InvitationRejectionListener smackInvitationRejectionListener;

    private ParticipantStatusListener smackParticipantStatusListener;

    private SubjectUpdatedListener smackSubjectUpdatedListener;

    private UserStatusListener smackUserStatusListener;

    /**
     * The smack multichat that we wrap.
     */
    private MultiUserChat smackMultiChat;

    /**
     * The jabber backend which has created this service.
     */
    private JabberBackend backend;

    /**
     * The context associated to this service.
     */
    private MultiChatContext context;

    /**
     * Receives the GROUP_CHAT messages.
     */
    private PacketListener smackMessageListener;

    /**
     * All packet listeners are stored here so we can remove them safely at the end.
     */
    private List<PacketListener> packetListeners;

    /**
     * Makes up a new multi-chat service based on XMPP: this construct
     * 
     * @param context
     * @param backend
     */
    public JabberMultiChatService( MultiChatContext context, JabberBackend backend ) {
        this();
        this.context = context;
        this.backend = backend;
    }

    /**
     * Default constructor 
     */
    public JabberMultiChatService() {
        super();
        this.multiChatListeners = new ArrayList<IMultiChatServiceListener>();
        this.invitationRejectedListeners = new ArrayList<IInvitationRejectedListener>();
        this.messageReceivedListeners = new ArrayList<IMessageReceivedListener>();
        this.userStatusListeners = new ArrayList<IUserStatusListener>();
        this.typingListeners = new ArrayList<ITypingEventListener>();
        this.managerEventListeners = new ArrayList<IManagerEventListener>();
        this.packetListeners = new ArrayList<PacketListener>();

        this.talkModel = new TalkModel();
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService#getContext()
     */
    public MultiChatContext getContext() {
        return context;
    }
    
    @Override
    public void setContext(INetworkServiceContext context) {
    	this.context = (MultiChatContext) context;    	
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#getChatRoomModel()
     */
    public IChatRoomModel getModel() {
        if (chatRoomModel == null)
            chatRoomModel = createModel();

        return chatRoomModel;
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#setChatRoomModel(it.uniba.di.cdg.xcore.multichat.model.IChatRoomModel)
     */
    public void setChatRoomModel( IChatRoomModel model ) {
        this.chatRoomModel = model;
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#setTalkModel(it.uniba.di.cdg.xcore.network.model.tv.ITalkModel)
     */
    public void setTalkModel( ITalkModel model ) {
        this.talkModel = model;
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#getTalkModel()
     */
    public ITalkModel getTalkModel() {
        return talkModel;
    }

    /**
     * @return Returns the backend.
     */
    public JabberBackend getBackend() {
        return backend;
    }

    /**
     * @param backend The backend to set.
     */
    public void setBackend( IBackend backend ) {
        this.backend = (JabberBackend) backend;
    }

    /**
     * @param context The context to set.
     */
    protected void setContext( MultiChatContext context ) {
        this.context = context;
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService#getRoomInfo(java.lang.String)
     */
    public IRoomInfo getRoomInfo( String room ) {
        try {
            return new RoomInfoAdapter( MultiUserChat.getRoomInfo( backend.getConnection(), room ) );
        } catch (XMPPException e) {
            e.printStackTrace();
            return null;
        }
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService#invite(java.lang.String, java.lang.String)
     */
    public void invite( String userId, String reason ) {
        smackMultiChat.invite( userId, reason );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#declineInvitation(it.uniba.di.cdg.xcore.multichat.InvitationEvent, java.lang.String)
     */
    public void declineInvitation( InvitationEvent event, String reason ) {
        MultiUserChat.decline( getBackend().getConnection(), event.getRoom(), event.getInviter(),
                reason );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#join()
     */
    public void join() throws NetworkServiceException {
        try {
            // If no nick name is provided we just use current connection jid
            if (context.getNickName() == null)
                context.setNickName( XMPPUtils.cleanJid( backend.getUserJid() ) );

            smackMultiChat = new MultiUserChat( backend.getConnection(), getRoomJid() );

            createRoom();

            createAndRegisterSmackListeners();
            
            DiscussionHistory history = new DiscussionHistory();
            history.setMaxStanzas(Integer.MAX_VALUE);

            if (context.hasPassword())
                smackMultiChat.join( context.getNickName(), context.getPassword(),
                		history, SmackConfiguration.getPacketReplyTimeout() );
            else
                smackMultiChat.join( context.getNickName(), "", history, 
                		SmackConfiguration.getPacketReplyTimeout() );                                         
            
            String userId = StringUtils.parseBareAddress(backend.getUserJid());            
    		if(context.getModerator()!=null && userId.equals(context.getModerator().getId()))
    			smackMultiChat.grantModerator(context.getNickName());
            
            updateInitialLocalUserStatus();
            
            // Notify listeners that we have joined ...
            for (IMultiChatServiceListener l : multiChatListeners)
                l.joined();
        } catch (XMPPException e) {
            throw new NetworkServiceException( e );
        }
    }

    /**
     * Create a new MUC Room if room doesn't exists    
     * @throws XMPPException 
     */
    private void createRoom() throws XMPPException{
    	boolean created = false;
    	//XXX actually the only way to know if the room already exists is the exception
    	String userId = StringUtils.parseBareAddress(backend.getUserJid());  
    	if(context.getModerator()!=null && userId.equals(context.getModerator().getId())){
    		try {    		
    			smackMultiChat.create(context.getNickName());
    			created = true;
    		}catch (XMPPException e) {			    		
    			System.out.println("Seems that room " + context.getRoom() +" already exists. " +
    					"Error: "+ e.getMessage());
    		} 
    		if(created){
    			//If we can't get exception means that room doesn't exists
    			smackMultiChat.sendConfigurationForm(new Form(Form.TYPE_SUBMIT));	
    			//    			Form form = smackMultiChat.getConfigurationForm();
    			//    			Form answerForm = form.createAnswerForm();
    			//    			answerForm.setAnswer("muc#roomconfig_moderatedroom", true);
    			//    			answerForm.setAnswer("muc#roomconfig_persistentroom", true);
    			//
    			//    			smackMultiChat.sendConfigurationForm(answerForm);    	
    		}
    	}else{			
    		//throw new XMPPException("Forbidden. Only the moderator can create a room");
    	}

    }

	/**
     * Update the initial participant status.
	 * 
     */
    private void updateInitialLocalUserStatus() {
        // XXX Wait a few time for the server to update its internal status
        Occupant me = null;
        String userJid = getLocalUserJid();
                
        int tentatives = 0;

        while (me == null && tentatives++ < 5) { // Let's hope the server will give us an aswer!
            System.out.println( String.format( "Waiting for server to notify myzelf ... (%s)",
                    userJid ) );
            try {
                Thread.sleep( 100 );
            } catch (InterruptedException e) { /* Silence */
            }

            me = smackMultiChat.getOccupant( userJid );
        }

        IParticipant localUser = createLocalParticipant();
     
        // XXX Jive server seems not to return the occupants for this room when we are _not_ moderator/ admin :S
        // So we simply use a default settings local user in this case :|
        // Update 20-09-09: seems that the string user in the room is lower case, so if the userJid contains
        // some upper case characters it is impossible recognize it
        if (me == null) {
            System.out
                    .println( "Warning: unable to get own user info from server. Using defaults." );
            
            localUser.setStatus( Status.JOINED );
            getModel().setLocalUser( localUser );            
            getModel().addParticipant(localUser);
            
        } else { // Server returned occupants info
            String s = String.format( "[Myzelf] %s  (%s) Affiliation: %s, role: %s", me.getNick(),
                    me.getJid(), me.getAffiliation(), me.getRole() );
            System.out.println( s );

            localUser.setRole( fromSmackRole( me.getRole() ) );
            localUser.setStatus( Status.JOINED );

            getModel().addParticipant(localUser);
            getModel().setLocalUser( localUser );

        }
    }

    /**
     * @param role string
     * @return role object
     */
    public static Role fromSmackRole( String role ) {
    	if ("moderator".equals( role ) || "owner".equals( role ))
    		return Role.MODERATOR;
    	else if ("participant".equals( role ))
    		return Role.PARTICIPANT;
    	return Role.VISITOR;
    }

    /**
     * Returns the jabbed id of the chat room: this is needed when broadcasting messages.
     * 
     * @return the jabber id 
     */
    private String getRoomJid() {
        return context.getRoom();
    }

    /**
     * Create and return an <code>IParticipant</code> which mocks the current local user.
     * 
     * @return the local participant
     */
    private IParticipant createLocalParticipant() {
        String id = getLocalUserJid();

        IParticipant local = new Participant( getModel(), id, context.getNickName(),
        		context.getPersonalStatus(), Role.PARTICIPANT );

        return local;
    }

    /**
     * Returns the jid of the local user (kind of "room@conference.server.com/MyNickName").
     * 
     * @return the jid of the local user
     */
    private String getLocalUserJid() {
        return getRoomJid() + "/" + context.getNickName();
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService#leave()
     */
    public void leave() {
        if (getBackend().isConnected()) {
            unregisterSmackListeners();
            smackMultiChat.leave();
        }
        smackMultiChat = null;

        // Notify listeners that we have left the chatroom ...
        for (IMultiChatServiceListener l : multiChatListeners)
            l.left();
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#sendMessage(java.lang.String, java.lang.String)
     */
    public void sendPrivateMessage( String userId, String message ) {
        final String localId = getModel().getLocalUser().getId();
        sendCustomPacket( new PrivateMessageIQPacket( localId,  StringUtils.escapeForXML(message )),  StringUtils.escapeForXML(userId) );

        IParticipant p = getModel().getParticipant( userId );
        notifyLocalSystemMessage( String.format( "[PM sent to %s] %s", p.getNickName(), message ) );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#sendMessage(java.lang.String)
     */
    public void sendMessage( String message ) {
        try {
            // here there's no need to escape the message
            smackMultiChat.sendMessage( message );
        } catch (XMPPException e) {
            e.printStackTrace();
        }
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#grantVoice(java.util.List)
     */
    public void grantVoice( List<String> nickNames ) {
        try {
            // Wants nicknames ...
            smackMultiChat.grantVoice( nickNames );
            // Wants jids ...
            //            smackMultiChat.grantMembership( users );
        } catch (XMPPException e) {
            e.printStackTrace();
        }
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#grantVoice(java.lang.String)
     */
    public void grantVoice( String nickName ) {
        try {
            getSmackMultiChat().grantVoice( nickName );
        } catch (XMPPException e) {
            e.printStackTrace();
        }
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#revokeVoice(java.util.List)
     */
    public void revokeVoice( List<String> nickNames ) {
        try {
            // Wants nicknames ...
            smackMultiChat.revokeVoice( nickNames );
            // Wants jids ...
            //            smackMultiChat.revokeMembership( users );
        } catch (XMPPException e) {
            e.printStackTrace();
        }
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#revokeVoice(java.lang.String)
     */
    public void revokeVoice( String nickName ) {
        try {
            getSmackMultiChat().revokeVoice( nickName );
        } catch (XMPPException e) {
            e.printStackTrace();
        }
    }

    /**
     * Create the model.
     * <p>
     * Derived classes may provide a different model implementation.
     * 
     * @return the model
     */
    protected IChatRoomModel createModel() {
        return new ChatRoomModel();
    }

    /**
     * Register the event listeners for smack events.
     */
    protected void createAndRegisterSmackListeners() {
        // Notify about invitation refusal.
        smackInvitationRejectionListener = new InvitationRejectionListener() {
            public void invitationDeclined( String invitee, String reason ) {
                System.out.println( String.format( "[SMACK] invitationDeclined(%s, %s)", invitee,
                        reason ) );
                for (IInvitationRejectedListener l : invitationRejectedListeners)
                    l.declined( invitee, reason );
            }
        };
        smackMultiChat.addInvitationRejectionListener( smackInvitationRejectionListener );

        smackParticipantStatusListener = new DefaultParticipantStatusListener() {
            /* (non-Javadoc)
             * @see org.jivesoftware.smackx.muc.DefaultParticipantStatusListener#nicknameChanged(java.lang.String, java.lang.String)
             */
            @Override
            public void nicknameChanged( String participantId, String nickName ) {
                System.out.println( String.format( "[SMACK] nickNameChanged( %s, %s )",
                        participantId, nickName ) );
                IParticipant p = getModel().getParticipant( participantId );
                if (p != null) {
                    //p.setNickName( nickName );
                    // XXX The server will notify a joined() after this which will make us to duplicate entries so
                    // we remove it now
                    getModel().removeParticipant( p );
                }
            }


            @Override
            public void joined( String participantId ) {
                System.out.println( String.format( "[SMACK] joined( %s )", participantId ) );
                IParticipant p = getModel().getParticipant( participantId );
                if (p == null) {
                    // The id we receive is something like "chatroom@conference.ugres.di.uniba.it/pippo",
                    // so we parse it and gain the real fields ...
                    String name = XMPPUtils.getUserNameFromChatString( participantId );
                    Occupant him = getSmackMultiChat().getOccupant( participantId );
                    Role role = fromSmackRole( him.getRole() );
                    p = new Participant( getModel(), participantId, name, "", role, Status.JOINED );
                    getModel().addParticipant( p );
                } else
                    p.setStatus( Status.JOINED );
            }
                        

            @Override
            public void left( String participantId ) {
                System.out.println( String.format( "[SMACK] left( %s )", participantId ) );
                IParticipant p = getModel().getParticipant( participantId );
                if (p == null) // TODO Is this check really necessary??
                    return;
                //p.setStatus( Status.NOT_JOINED );
                getModel().removeParticipant( p );
            }

            @Override
            public void voiceGranted( String participantId ) {
                System.out.println( String.format( "[SMACK] voiceGranted( %s )", participantId ) );
                IParticipant p = chatRoomModel.getParticipant( participantId );
                if (p == null)
                    return;
                p.setStatus( Status.JOINED );
                notifyLocalSystemMessage( String.format( "Moderator has allowed %s back "
                        + "in conversation", p.getNickName() ) );
            }

            @Override
            public void voiceRevoked( String participantId ) {
                System.out.println( String.format( "[SMACK] voiceRevoked( %s )", participantId ) );
                IParticipant p = chatRoomModel.getParticipant( participantId );
                if (p == null)
                    return;
                p.setStatus( Status.FROZEN );
                notifyLocalSystemMessage( String.format( "Moderator has stopped %s from "
                        + "contributing to conversation", p.getNickName() ) );
            }
        };
        smackMultiChat.addParticipantStatusListener( smackParticipantStatusListener );

        smackSubjectUpdatedListener = new SubjectUpdatedListener() {
            public void subjectUpdated( String subject, String from ) {
                System.out
                        .println( String.format( "[SMACK] subjectUpdated(%s, %s)", subject, from ) );
                getModel().setSubject( subject, from );
            }
        };
        smackMultiChat.addSubjectUpdatedListener( smackSubjectUpdatedListener );

        smackUserStatusListener = new DefaultUserStatusListener() {
            /* (non-Javadoc)
             * @see org.jivesoftware.smackx.muc.DefaultUserStatusListener#moderatorGranted()
             */
            @Override
            public void moderatorGranted() {
                System.out.println( "[SMACK] moderatorGranted()" );
                getModel().getLocalUser().setRole( Role.MODERATOR );
                for (IUserStatusListener l : userStatusListeners)
                    l.moderatorGranted();
            }

            /* (non-Javadoc)
             * @see org.jivesoftware.smackx.muc.DefaultUserStatusListener#moderatorRevoked()
             */
            @Override
            public void moderatorRevoked() {
                System.out.println( "[SMACK] moderatorRevoked()" );
                getModel().getLocalUser().setRole( Role.VISITOR );
                for (IUserStatusListener l : userStatusListeners)
                    l.moderatorRevoked();
            }

            /* (non-Javadoc)
             * @see org.jivesoftware.smackx.muc.DefaultUserStatusListener#ownershipGranted()
             */
            @Override
            public void ownershipGranted() {
                System.out.println( "[SMACK] ownershipGranted()" );
                for (IUserStatusListener l : userStatusListeners)
                    l.ownershipGranted();
            }

            /* (non-Javadoc)
             * @see org.jivesoftware.smackx.muc.DefaultUserStatusListener#ownershipRevoked()
             */
            @Override
            public void ownershipRevoked() {
                System.out.println( "[SMACK] ownershipRevoked()" );
                for (IUserStatusListener l : userStatusListeners)
                    l.ownershipRevoked();
            }

            /* (non-Javadoc)
             * @see org.jivesoftware.smackx.muc.DefaultUserStatusListener#voiceGranted()
             */
            @Override
            public void voiceGranted() {
                System.out.println( "[SMACK] voiceGranted()" );
                getModel().getLocalUser().setStatus( Status.JOINED );
                for (IUserStatusListener l : userStatusListeners)
                    l.voiceGranted();
            }

            /* (non-Javadoc)
             * @see org.jivesoftware.smackx.muc.DefaultUserStatusListener#voiceRevoked()
             */
            @Override
            public void voiceRevoked() {
                System.out.println( "[SMACK] voiceRevoked()" );
                getModel().getLocalUser().setStatus( Status.FROZEN );
                for (IUserStatusListener l : userStatusListeners)
                    l.voiceRevoked();
            }
        };
        smackMultiChat.addUserStatusListener( smackUserStatusListener );

        // Collect multi-chat messages
        smackMessageListener = new PacketListener() {
            // Note that we are ensured by SMACK that will receive GROUP_CHAT messages only ...
            public void processPacket( Packet packet ) {
                // Normal message then ...
                Message smackMessage = (Message) packet;
                IMessage incoming = new MultiChatMessage( getModel(), smackMessage.getFrom(),
                        smackMessage.getBody() );

                getTalkModel().addEntry( makeEntryFromMessage( incoming ) );
            }
        };
        smackMultiChat.addMessageListener( smackMessageListener );

        PacketListener packetListener = null;
        // Grab incoming typing notification packets ...
        packetListener = new PacketListener() {
            public void processPacket( Packet packet ) {
                final TypingNotificationPacket typingPacket = (TypingNotificationPacket) packet
                        .getExtension( TypingNotificationPacket.ELEMENT_NAME,
                        		TypingNotificationPacket.ELEMENT_NS );

                TypingEvent typingEvent = new TypingEvent( typingPacket.getWho() );
                for (ITypingEventListener l : typingListeners)
                    l.onTyping( typingEvent );
            }
        };
        addPacketListener( packetListener, new MultiChatTypingFilter( backend.getUserJid(),
                createLocalParticipant().getId() ) );

        // Notify listeners that the view is read-only now.
        packetListener = new PacketListener() {
            public void processPacket( Packet packet ) {
                final ViewReadOnlyPacket ext = (ViewReadOnlyPacket) packet.getExtension(
                        ViewReadOnlyPacket.ELEMENT_NAME, SmackCommons.CDG_NAMESPACE );

                ViewReadOnlyEvent event = new ViewReadOnlyEvent( ext.getViewId(), ext.isReadOnly() );
                for (IManagerEventListener l : managerEventListeners)
                    l.onManagerEvent( event );
            }
        };
        addPacketListener( packetListener, ViewReadOnlyPacket.FILTER );

        // Private messages are implemented as custom IQs: so here it is the packet listener ... 
        packetListener = new PacketListener() {
            public void processPacket( Packet packet ) {
                final PrivateMessageIQPacket pm = (PrivateMessageIQPacket) packet;

                final IParticipant p = getLocalUserOrParticipant( pm.getWho() );

                if (p == null)
                    return;
                for (IMessageReceivedListener l : messageReceivedListeners)
                    l.privateMessageReceived( pm.getText(), p.getNickName() );
            }
        };
        addPacketListener( packetListener, PrivateMessageIQPacket.FILTER );
    }

    /**
     * @param incoming
     * @return
     */
    protected Entry makeEntryFromMessage( IMessage incoming ) {
        final Date now = Calendar.getInstance().getTime();

        Entry entry = new Entry();
        entry.setTimestamp( now );
        entry.setText( incoming.getText() );

        IParticipant p = getLocalUserOrParticipant( incoming.getFrom() );
        if (p != null)
            entry.setWho( p.getNickName() );
        else
            entry.setWho( incoming.getFrom() );

        return entry;
    }

    /**
     * Notify a message to the manager: this is usually meant for displaying some system
     * status to the user (i.e. another user frozen).
     * <p>
     * Note that this message WON'T notify anything to remote clients: it is only meant as
     * a display on local client.
     * 
     * @param message
     */
    protected void notifyLocalSystemMessage( String message ) {
        //        SystemMessage sysMessage = new SystemMessage( message );
        final Date now = Calendar.getInstance().getTime();

        Entry entry = new Entry();
        entry.setTimestamp( now );
        entry.setWho( "***" );
        entry.setText( message );

        getTalkModel().addEntry( entry );
    }

    /**
     * Unregister the smack chat listeners registered before. 
     */
    protected void unregisterSmackListeners() {
        smackMultiChat.removeUserStatusListener( smackUserStatusListener );
        smackMultiChat.removeSubjectUpdatedListener( smackSubjectUpdatedListener );
        smackMultiChat.removeParticipantStatusListener( smackParticipantStatusListener );
        smackMultiChat.removeInvitationRejectionListener( smackInvitationRejectionListener );
        smackMultiChat.removeMessageListener( smackMessageListener );

        if (backend.getConnection() != null) {
            for (PacketListener l : packetListeners)
                backend.getConnection().removePacketListener( l );
        }
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#changeSubject(java.lang.String)
     */
    public void changeSubject( String subject ) {
        try {
            smackMultiChat.changeSubject( subject );
        } catch (XMPPException e) {
            // XXX It seems that the Jive WildFire does not respond initially, making SMACK to throw
            // "No response from server" exception :S
            e.printStackTrace();
        }
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#addMessageReceivedListener(it.uniba.di.cdg.xcore.multichat.service.IMessageReceivedListener)
     */
    public void addMessageReceivedListener( IMessageReceivedListener listener ) {
        messageReceivedListeners.add( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#removeMessageReceivedListener(it.uniba.di.cdg.xcore.multichat.service.IMessageReceivedListener)
     */
    public void removeMessageReceivedListener( IMessageReceivedListener listener ) {
        messageReceivedListeners.remove( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#addMultiChatListener(it.uniba.di.cdg.xcore.multichat.service.IMultiChatService.IMultiChatServiceListener)
     */
    public void addServiceListener( IMultiChatServiceListener listener ) {
        multiChatListeners.add( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#removeMultiChatListener(it.uniba.di.cdg.xcore.multichat.service.IMultiChatService.IMultiChatServiceListener)
     */
    public void removeServiceListener( IMultiChatServiceListener listener ) {
        multiChatListeners.remove( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService#addInvitationRejectedListener(it.uniba.di.cdg.xcore.multichat.IMultiChatService.IInvitationRejectedListener)
     */
    public void addInvitationRejectedListener( IInvitationRejectedListener listener ) {
        invitationRejectedListeners.add( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.IMultiChatService#removeInvitationRejectedListener(it.uniba.di.cdg.xcore.multichat.IMultiChatService.IInvitationRejectedListener)
     */
    public void removeInvitationRejectedListener( IInvitationRejectedListener listener ) {
        invitationRejectedListeners.remove( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.events.ITypingListener#typing()
     */
    public void typing() {
        System.out.println( "Notify typing ..." );
        TypingNotificationPacket notification = new TypingNotificationPacket();
        notification
                .setWho( StringUtils.escapeForXML( getModel().getLocalUser().getNickName() ) );

        Message msg = smackMultiChat.createMessage();
        msg.setTo( getRoomJid() );
        msg.addExtension( notification );

        System.out.println( "Notifying: " + msg.toXML() );

        sendPacket( msg );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.events.ITypingNotificationSupport#addTypingEventListener(it.uniba.di.cdg.xcore.network.events.ITypingEventListener)
     */
    public void addTypingEventListener( ITypingEventListener listener ) {
        typingListeners.add( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.network.events.ITypingNotificationSupport#removeTypingEventListener(it.uniba.di.cdg.xcore.network.events.ITypingEventListener)
     */
    public void removeTypingEventListener( ITypingEventListener listener ) {
        typingListeners.remove( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#addUserStatusListener(it.uniba.di.cdg.xcore.multichat.service.IUserStatusListener)
     */
    public void addUserStatusListener( IUserStatusListener listener ) {
        userStatusListeners.add( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#removeUserStatusListener(it.uniba.di.cdg.xcore.multichat.service.IUserStatusListener)
     */
    public void removeUserStatusListener( IUserStatusListener listener ) {
        userStatusListeners.add( listener );
    }

    /**
     * Returns the undelying smack multi-user chat.
     * 
     * @return Returns the smackMultiChat.
     */
    protected MultiUserChat getSmackMultiChat() {
        return smackMultiChat;
    }

    /**
     * Send a  packet though the current connection. Note that the packet must already be constructed
     * and all fields initialized.
     * 
     * @param packet
     */
    protected void sendPacket( Packet packet ) {
        backend.getConnection().sendPacket( packet );
    }

    /**
     * Shortcut for sending custom extension packets: they will be wrapped in the a SMACK
     * message.
     * 
     * @param ext
     */
    protected void sendCustomExtension( PacketExtension ext ) {
        Message message = getSmackMultiChat().createMessage();
        message.addExtension( ext );
        // FIXME escape reserved xml chars
        sendPacket( message );
    }

    protected void sendCustomExtension( PacketExtension ext, String to ) {
        Message message = getSmackMultiChat().createMessage();
        message.setTo( to );
        message.addExtension( ext );

        sendPacket( message );
    }
    
    protected void sendCustomExtension(IPacketExtension ext){
    	sendCustomExtension(PacketExtensionAdapter.adaptToTargetPacketExtension(ext));
    }

    /**
     * Shortcut for sending a custom packet (typically IQs).
     * 
     * @param packet the custom packet
     * @param to the id of the paticipant (something like "room@conference.server.net/Pippo").
     */
    protected void sendCustomPacket( Packet packet, String to ) {
        packet.setTo( to );

        sendPacket( packet );
    }

    /**
     * Add a packet listener for capturing packets matching a particular filter. Note that this 
     * method will also track the listener: when the chat closes all the listeners will be
     * automatically removed. 
     * 
     * @param listener
     * @param filter
     */
    protected void addPacketListener( PacketListener listener, PacketFilter filter ) {
        backend.getConnection().addPacketListener( listener, filter );
        packetListeners.add( listener );
    }

    /**
     * Remove a previosly registered packet listener.
     * 
     * @param listener
     */
    protected void removePacketListener( PacketListener listener ) {
        backend.getConnection().removePacketListener( listener );
        packetListeners.remove( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#notifyViewReadOnly(java.lang.String, boolean)
     */
    public void notifyViewReadOnly( String viewId, boolean readOnly ) {
        sendCustomExtension( new ViewReadOnlyPacket( viewId, readOnly ) );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#addManagerEventListener(it.uniba.di.cdg.xcore.multichat.events.IManagerEventListener)
     */
    public void addManagerEventListener( IManagerEventListener listener ) {
        managerEventListeners.add( listener );
    }

    /* (non-Javadoc)
     * @see it.uniba.di.cdg.xcore.multichat.service.IMultiChatService#removeManagerEventListener(it.uniba.di.cdg.xcore.multichat.events.IManagerEventListener)
     */
    public void removeManagerEventListener( IManagerEventListener listener ) {
        managerEventListeners.remove( listener );
    }

    /**
     * Returns the participant, searching between the local and the others remote.
     * 
     * @param who the id of the participant to search
     * @return the participant or <code>null</code> if the id is unknown
     */
    protected IParticipant getLocalUserOrParticipant( String who ) {
        return getModel().getLocalUserOrParticipant( who );
    }

    /**
     * Provides internal thread synchronization.
     */
    @Aspect
    public static class OwnThreadSafety extends ThreadSafetyAspect {
        /* (non-Javadoc)
         * @see it.uniba.di.cdg.xcore.aspects.ThreadSafety#readOperations()
         */
        @Pointcut("execution( public void org.jivesoftware.smackx.muc.DefaultUserStatusListener+.*(..) )")
        protected void readOperations() {
        }

        /* (non-Javadoc)
         * @see it.uniba.di.cdg.xcore.aspects.ThreadSafety#writeOperations()
         */
        @Pointcut("execution( private void JabberMultiChatService.updateFirstTime() )"
                + "|| execution( public void org.jivesoftware.smackx.muc.DefaultParticipantStatusListener+.*(..) )"
                + "|| execution( public void org.jivesoftware.smackx.muc.SubjectUpdatedListener+.*(..) )")
        protected void writeOperations() {
        }
    }
}
